/**
 * Copyright(c) 2003-2007 Observant Pty Ltd
 * All rights reserved.
 */
package com.ctn.util.dao;

// java core
import java.io.Serializable;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TimeZone;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;

import com.ctn.util.exception.InvalidResourceQueryException;

/**
 * This class is capture the parameters of a query, which is used to invoke a
 * find or similar on a data access object.
 * <p>
 * Here are some examples of query strings
 * <p>
 * id=1
 * 	find the records with an id of value 1
 * fieldUnitAddress=3&order=id
 *  find the records where fieldUnitAddress=3 and sort the results set in
 *  ascending order using the 'id' property
 * syslogEntryDate$from=2007-03-20&stslogEntryDate$to=2007-04-03&first=4
 *  find the records between dates 2007-03-20 and 2007-04-03 inclusive and
 *  return the result set from the 4th record inwards
 * channelDeploymentId=label1&order=id&count=10
 *  return the first 10 records where channelDeploymentId=label1 and ensure
 *  that the result set id ordered by the 'id' property.
 *
 * @author jima
 */
public class ResourceQuery implements Serializable {

	/**
	 * Default SUID
	 */
	private static final long serialVersionUID = 1L;

	/**
	 * The query string
	 */
	private final String queryString;

	/**
	 * A map of property-value pairs.
	 */
	private final ResourceQueryParams queryParams;

	/**
	 * Indicates  the sort order of the result set
	 */
	private String order;

	/**
	 * Whether to order the result set in ascending order
	 */
	private boolean asc = true;

	/**
	 * The first record to be returned
	 */
	private int first = 0;

	/**
	 * The number of records to be returned
	 */
	private int count = 0;

	/**
	 * the name of the JSONP callback function
	 */
	private String callbackFunction;

	/**
	 * the name of the fiel for the requested download
	 */
	private String downloadFileName;

	/**
	 * should the results be wrapped in a {@link ResourceQueryResult}.<br/>
	 * This is normally used in JSONP style interactions, but may be generally useful for any paging style API
	 */
	private boolean wrapResult = false;

	/**
	 * allow the query client to specify their UTC offset so result can be made relative to the requesting timezone
	 */
	private int requestorUTCOffsetMinutes = 0;

	private TimeZone requestorTimeZone;

	private boolean forceDownload = false;

	/**
	 * Default constructor
	 */
	public ResourceQuery() {
		this(null);
	}

	/**
	 * Construct an instance using a query string and a hash map of template
	 * params
	 *
	 * @param queryString
	 *  		the query string to parse
	 * @param templateParams
	 * 			the additional template parameters
	 */
	public ResourceQuery(String queryString, Map<String, String> templateParams) {
		this(queryString);

		// add the template params
		for (String param : templateParams.keySet()) {
			queryParams.put(param, templateParams.get(param));
		}
	}

	public static boolean isJavaIdentifier(String s) {
		if (s.length() == 0 || !Character.isJavaIdentifierStart(s.charAt(0))) {
			return false;
		}
		for (int i=1; i<s.length(); i++) {
			if (!Character.isJavaIdentifierPart(s.charAt(i))) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Construct an instance using a query string. The query string must
	 * support the format specified in the header
	 *
	 * @param queryString
	 * 		the query string to parse
	 */
	public ResourceQuery(String queryString) {
		this.queryString = queryString;
		this.queryParams = new ResourceQueryParams();
		boolean isEmptyValue = false;

		if (StringUtils.isNotEmpty(queryString)) {
			/**
			 * Process the query string and override any values
			 */
			StringTokenizer attributes = new StringTokenizer(queryString, "&");
			while (attributes.hasMoreTokens()) {
				isEmptyValue = false;
				String attribute = attributes.nextToken();
				if (StringUtils.containsNone(attribute, "=")) {
					// this could be a param with no value, meaning IS NULL
					if (!isJavaIdentifier(attribute)) {						
						throw new InvalidResourceQueryException(queryString,
								"[" + attribute + "] is not valid. "
								+ "Only the '=' operator is supported.");
					}
					else {
						isEmptyValue = true;
					}
				}

				String key = null;
				String value = null;

				if(isEmptyValue) {
					key = attribute;
					value = "";
				}
				else {
					String[] params = StringUtils.split(attribute, "=", 2);
					if (params.length == 1) {
						if(attribute.endsWith("=")) {
							isEmptyValue = true;
							key = params[0];
							value = "";
						}
						else {
							throw new InvalidResourceQueryException(queryString);
						}
					}
					else {
						if (params.length != 2) {
							throw new InvalidResourceQueryException(queryString);
						}
						else {
							key = params[0];
							value = params[1];
						}
					}
				}

				if (StringUtils.equals(key, "first")||StringUtils.equals(key, "start")) {
					try {
						first = Integer.parseInt(value);
					} catch (NumberFormatException exception) {
						throw new InvalidResourceQueryException(queryString,
								"Invalid value for 'first':[" + value + "] in query: "
								+ queryString, exception);
					}
				} else if (StringUtils.equals(key, "count")||StringUtils.equals(key, "limit")) {
					try {
						count = Integer.parseInt(value);
					} catch (NumberFormatException exception) {
						throw new InvalidResourceQueryException(queryString,
								"Invalid value for 'count':[" + value + "] in query: "
								+ queryString, exception);
					}
				} else if (StringUtils.equals(key, "asc")) {
					asc = Boolean.parseBoolean(value);
				} else if (StringUtils.equals(key, "dir")) {
					asc = StringUtils.equalsIgnoreCase(value,"asc");
				} else if (StringUtils.equals(key, "order")||StringUtils.equals(key, "sort")) {
					order = value;
				} else if (StringUtils.equals(key, "media")){
					// drop the media key value pair since it is used
					// by restlet app
				} else if (key.startsWith("_dc")){
					// ignore this it is an anti-caching token
				} else if (key.startsWith("_ruom")){
					try {
						requestorUTCOffsetMinutes = Integer.parseInt(value);
					} catch (NumberFormatException exception) {
						throw new InvalidResourceQueryException(queryString,
								"Invalid value for '_uom':[" + value + "] in query: "
								+ queryString, exception);
					}
				} else if (key.startsWith("_timezoneID")){
					requestorTimeZone = TimeZone.getTimeZone(value);
				} else if (StringUtils.equals(key, "_filename")) {
					// for download file
					downloadFileName = value;
				} else if (StringUtils.equals(key, "_forceDownload")) {
					// for download file
					forceDownload = Boolean.parseBoolean(value);
				} else if (StringUtils.equals(key, "callback")||key.startsWith("_callback")) {
					// JSONP support requires specification of a callback function
					callbackFunction = value;
				} else if (key.startsWith("_wrap")) {
					// wrap returned resources in query result object to provide query result meta-data
					setWrapResult(Boolean.valueOf(value));
				} else if (key.equals("_")) {
					// this is a no-cache token that comes from JSON JS libraries, ignore
				} else {
					queryParams.put(key, value);
				}
			}
		}
	}

	/**
	 * @return the asc
	 */
	public boolean isAsc() {
		return asc;
	}

	/**
	 * @return the count
	 */
	public int getCount() {
		return count;
	}

	/**
	 * @return the first
	 */
	public int getFirst() {
		return first;
	}

	/**
	 * @return the order
	 */
	public String getOrder() {
		return order;
	}

	/**
	 * @return the query
	 */
	public ResourceQueryParams getQueryParams() {
		return queryParams;
	}

	/**
	 * @return the queryString
	 */
	public String getQueryString() {
		return queryString;
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return new ToStringBuilder(this).toString();
	}

	public String getCallbackFunction() {
		return callbackFunction;
	}

	@Override
	public boolean equals(Object obj) {
		return EqualsBuilder.reflectionEquals(this, obj);
	}

	public String toURLForm() {
		StringBuilder builder = new StringBuilder();
		if ( this.getOrder() != null ) {
			appendURLParam(builder, "dir", isAsc()?"asc":"desc");
			appendURLParam(builder, "order", getOrder());
		}
		appendURLParam(builder, "first", String.valueOf(getFirst()));
		appendURLParam(builder, "count", String.valueOf(getCount()));
		if ( this.wrapResult ) {
			appendURLParam(builder, "_wrap_", "true");
		}
		if ( this.getCallbackFunction() != null ) {
			appendURLParam(builder, "callback", getCallbackFunction());
		}

		builder.append("&");
		builder.append(this.queryParams.toURLForm());

		return builder.toString();
	}

	/**
	 * 
	 * @author jima
	 *
	 */
	public static class ResourceQueryParams implements Serializable {
		private static final long serialVersionUID = 1L;

		public ResourceQueryParams() {
		}

		Map<String, ResourceQueryParamValue> actualMap = new HashMap<String, ResourceQueryParamValue>();

		public Set<String> keySet() {
			return actualMap.keySet();
		}

		public ResourceQueryParamValue get(String key) {
			return actualMap.get(key);
		}

		public void put(String key, String value) {
			if ( actualMap.containsKey(key)) {
				actualMap.get(key).addValue(value);
			}
			else {
				actualMap.put(key, new ResourceQueryParamValue(value));
			}
		}

		/**
		 * make sure this is the only/single value associated with this key
		 * @param key
		 * @param value
		 */
		public void putSingle(String key, String value) {
			if ( actualMap.containsKey(key)) {
				actualMap.get(key).setValue(value);
			}
			else {
				actualMap.put(key, new ResourceQueryParamValue(value));
			}
		}

		public void removeParam(String paramKey) {
			actualMap.remove(paramKey);
		}

		public String toURLForm() {
			StringBuilder builder = new StringBuilder();
			for (String key : actualMap.keySet()) {
				String[] values = actualMap.get(key).getValues();
				for (int i = 0; i < values.length; i++) {
					appendURLParam(builder, key, values[i]);
				}
			}
			return builder.toString();
		}

		public int getParamCount() {
			return actualMap.size();
		}

		@Override
		public boolean equals(Object obj) {
			if (!(obj instanceof ResourceQueryParams)) {
				return false;
			}
			return actualMap.equals(((ResourceQueryParams)obj).actualMap);
		}
	}

	/**
	 * 
	 * @param builder
	 * @param key
	 * @param value
	 */
	@SuppressWarnings("deprecation")
	protected static void appendURLParam(StringBuilder builder, String key,
			String value) {
		if ( builder.length() > 0 ) {
			builder.append("&");
		}
		builder.append(key);
		builder.append("=");
		builder.append(URLEncoder.encode(value));
	}

	public static class ResourceQueryParamValue implements Serializable {
		private static final long serialVersionUID = 1L;

		boolean multiValued = false;

		List<String> values = new ArrayList<String>();

		ResourceQueryParamValue(String value) {
			setValue(value);
		}

		public void setValue(String value) {
			values.clear();
			values.add(value);
			setMultiValued(false);
		}

		public void addValue(String value) {
			values.add(value);
			setMultiValued(true);
		}

		public boolean isMultiValued() {
			return multiValued;
		}

		void setMultiValued(boolean multi) {
			this.multiValued = multi;
		}

		public String getValue() {
			return (values.size()>0)?values.get(0):null;
		}

		public String[] getValues() {
			return values.toArray(new String[values.size()]);
		}

		@Override
		public boolean equals(Object obj) {
			if (!(obj instanceof ResourceQueryParamValue)) {
				return false;
			}
			return values.equals(((ResourceQueryParamValue)obj).values);
		}
	}

	public boolean isWrapResult() {
		return wrapResult;
	}

	public void setWrapResult(boolean wrapResult) {
		this.wrapResult = wrapResult;
	}

	public void setOrder(String order) {
		this.order = order;
	}

	public void setAsc(boolean asc) {
		this.asc = asc;
	}

	public void setFirst(int first) {
		this.first = first;
	}

	public void setCount(int count) {
		this.count = count;
	}

	public void setCallbackFunction(String callbackFunction) {
		this.callbackFunction = callbackFunction;
	}

	public int getRequestorUTCOffsetMinutes() {
		return requestorUTCOffsetMinutes;
	}

	public void setRequestorUTCOffsetMinutes(int requestorUTCOffsetMinutes) {
		this.requestorUTCOffsetMinutes = requestorUTCOffsetMinutes;
	}

	public String getDownloadFileName() {
		return downloadFileName;
	}

	public void setDownloadFileName(String downloadFileName) {
		this.downloadFileName = downloadFileName;
	}

	public TimeZone getRequestorTimeZone() {
		return requestorTimeZone;
	}

	public void setRequestorTimeZone(TimeZone requestorTimeZone) {
		this.requestorTimeZone = requestorTimeZone;
	}

	public boolean isForceDownload() {
		return forceDownload;
	}

	public void setForceDownload(boolean forceDownload) {
		this.forceDownload = forceDownload;
	}

}
